/*******************************************************************************
 *
 * Copyright (c) 2008-2011 ayound@gmail.com, guwei8576431@gmail.com
 * This program and the accompanying materials
 * are made available under the terms of the GNU GENERAL PUBLIC LICENSE
 * which accompanies this distribution, and is available at
 * http://dev.perl.org/licenses/gpl1.html
 * All rights reserved.
 *
 * Created on 2009-10-20
 *******************************************************************************/

package org.cgtw.scriptprofile.engine.compile.impl;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.cgtw.scriptprofile.engine.compile.FunctionInfo;
import org.cgtw.scriptprofile.engine.compile.IScriptCompileEngine;

public class ScriptCompileEngine implements IScriptCompileEngine {
	private int funcIndex = 0;

	private Map<Integer, FunctionInfo> functionMap = new HashMap<Integer, FunctionInfo>();

	public String compileScript(String url, String text) {
		return compileJs(url, text);
	}
	public FunctionInfo getFunctionInfo(int functionIndex) {
		return functionMap.get(functionIndex);
	}
	/**
	 * compile Javascript to profile script
	 * add profile scripts before and after every function body
	 * make a function index map of url,function name and function line
	 * @param url
	 * @param text
	 * @return
	 */
	private String compileJs(String url, String text) {

		text = text.replaceAll("\n\r|\r\n|\n|\r", "\n");
		boolean isHTML = false;
		if (text.trim().startsWith("<")) {
			isHTML = true;
		}
		// the result builder
		StringBuilder result = new StringBuilder();
		if (isHTML) {
			result.append(getProfileScript());
		} else {
			result.append(getProfileJs());
		}

		int length = text.length();
		if (length == 0) {
			return "";
		}

		int currLine = 1;
		// function count tag,when read "function" founctionCount++ ,when the
		// function body end
		// founctionCount--
		int functionCount = 0;
		Map<Integer, Boolean> functionStartMap = new HashMap<Integer, Boolean>();
		Map<Integer, Integer> functionBodyTag = new HashMap<Integer, Integer>();
		boolean singleNote = false;

		boolean mutiNote = false;

		boolean singleMark = false;

		boolean doubleMark = false;

		boolean isReg = false;

		boolean htmlScriptStart = false;

		boolean inScript = true;

		if (isHTML) {
			inScript = false;
		}

		for (int i = 0; i < length; ++i) {
			char ch = text.charAt(i);
			if (ch == '\n') {
				currLine++;
				singleNote = false;
				singleMark = false;
				doubleMark = false;
				isReg = false;
				result.append(ch);
				continue;
			}
			if (isHTML) {
				if (ch == '<') {
					if (inScript) {
						if ("/script".equalsIgnoreCase(getNextString(text, i,
								length, 7))) {
							inScript = false;
							htmlScriptStart = false;
						}
					} else {
						if ("script".equalsIgnoreCase(getNextString(text, i,
								length, 6))) {
							htmlScriptStart = true;
						}
					}
				} else if (ch == '>') {
					if (htmlScriptStart) {
						inScript = true;
						htmlScriptStart = false;
					}
				}
			}
			if (!inScript) {
				result.append(ch);
				continue;
			}
			switch (ch) {
			case 'f':
				if (!(singleNote || mutiNote || singleMark || doubleMark || isReg)) {
					if ("unction".equals(getNextString(text, i, length, 7))) {
						char next8ch = getNext(text, i, length, 8);
						if (next8ch == ' ' || next8ch == '(' || next8ch == '\n') {
							functionCount++;
							functionStartMap.put(functionCount, true);
							functionBodyTag.put(functionCount, 0);
							synchronized (this) {
								funcIndex++;
							}
						}
					}
				}
				result.append(ch);
				break;
			case '{':
				result.append(ch);
				if (!(singleNote || mutiNote || singleMark || doubleMark || isReg)) {
					if (functionCount > 0) {
						functionBodyTag.put(functionCount, functionBodyTag
								.get(functionCount) + 1);
						if (functionStartMap.containsKey(functionCount)
								&& functionStartMap.get(functionCount)) {
							String functionName = getFunctionName(text, i);

							result.append("arguments[\"__id\"]= ++_profileIndex;\n");
							result.append("var __profile = [_profileIndex," + funcIndex
									+ " ,(new Date()).getTime()];\n");
							FunctionInfo info = new FunctionInfo(url, currLine,
									functionName);
							functionMap.put(funcIndex, info);
							result.append("var __result = function(){\n");
							functionStartMap.put(functionCount, false);
						}
					}
				}
				break;
			case '}':
				if ('\\' == getPre(text, i, length)) {
					result.append(ch);
					break;
				}
				if (!(singleNote || mutiNote || singleMark || doubleMark || isReg)) {
					if (functionCount > 0) {
						functionBodyTag.put(functionCount, functionBodyTag
								.get(functionCount) - 1);
						if (functionBodyTag.get(functionCount) == 0) {
							functionStartMap.put(functionCount, false);
							functionCount--;
							result
									.append("}.apply(this,arguments);\n__profile.push((new Date()).getTime());\n$jsprofile(__profile,arguments);return __result;\n");
						}
					}
				}
				result.append(ch);
				break;
			case '/':
				result.append(ch);
				if ('\\' == getPre(text, i, length)) {
					break;
				}
				char next = getNext(text, i, length);
				if (mutiNote) {
					if ('*' == getPre(text, i, length)) {
						mutiNote = false;
					}
				}
				if (next == '/') {
					singleNote = true;
				} else if (next == '*') {
					mutiNote = true;
				} else if (next == '=') {
					break;
				} else {
					if (!(singleNote || mutiNote || singleMark || doubleMark)) {
						if (isReg) {
							isReg = false;
						} else {
							isReg = readRegExp(text, i, length);
						}
					}
				}
				break;
			case '\'':
				result.append(ch);
				if ('\\' == getPre(text, i, length)) {
					if ('\\' != getPre(text, i - 1, length)) {
						break;
					}
				}
				if (!(singleNote || mutiNote || doubleMark || isReg)) {
					if (singleMark) {
						singleMark = false;
					} else if (!doubleMark) {
						singleMark = true;
					}
				}
				break;
			case '"':
				result.append(ch);
				if ('\\' == getPre(text, i, length)) {
					if ('\\' != getPre(text, i - 1, length)) {
						break;
					}
				}
				if (!(singleNote || mutiNote || singleMark || isReg)) {
					if (doubleMark) {
						doubleMark = false;
					} else if (!singleMark) {
						doubleMark = true;
					}
				}
				break;
			default:
				result.append(ch);
				break;
			}
		}

		return result.toString();
	}
	/**
	 * return script and tag for html file file
	 * @return
	 */
	private static CharSequence getProfileScript() {
		return "<script>" + getProfileJs() + "</script>";
	}
	/**
	 * get profile function script for javascript and html file
	 * @return
	 */
	private static String getProfileJs() {
		try {
			return IOUtils.toString(ScriptCompileEngine.class
					.getResourceAsStream("profile.js"));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return "";
	}
	/**
	 * get function name of current function
	 * find it from privous string.
	 * @param text
	 * @param pos
	 * @return
	 */
	private static String getFunctionName(String text, int pos) {
		String preStr = text.substring(0, pos);
		int funcPos = preStr.lastIndexOf("function");
		int leftPos = preStr.lastIndexOf('(');
		String functionName = "anonymous";
		if (leftPos > funcPos && funcPos > -1
				&& (funcPos + 8) < preStr.length()) {
			functionName = preStr.substring(funcPos + 8, leftPos);
		}
		if (functionName.trim().length() > 0) {
			return functionName.trim();
		} else {
			String leftStr = preStr.substring(0, funcPos).trim();
			if (leftStr.endsWith("=")) {
				leftStr = leftStr.substring(0, leftStr.length() - 1).trim();
				StringBuilder buffer = new StringBuilder();
				for (int i = leftStr.length() - 1; i > -1; i--) {
					char ch = leftStr.charAt(i);
					if (Character.isLetterOrDigit(ch) || ch == '_' || ch == '$') {
						buffer.append(ch);
					} else {
						break;
					}
				}
				String funcStr = buffer.toString();
				if (funcStr.length() > 0) {
					return StringUtils.reverse(funcStr);
				}
			}
		}
		if (functionName == null || functionName.trim().length() == 0) {
			functionName = "anonymous";
		}
		return functionName;
	}
	/**
	 * check if the String in // is a regExp
	 * @param text
	 * @param i
	 * @param length
	 * @return
	 */
	private static boolean readRegExp(String text, int i, int length) {
		StringBuilder buffer = new StringBuilder();

		for (int j = i + 1; j < length; j++) {
			char c = text.charAt(j);
			if (c == '\n') {
				return false;
			} else if (c == '/') {
				if (getPre(text, i, length) != '\\') {
					break;
				} else {
					continue;
				}
			}
			buffer.append(c);
		}
		try {
			Pattern.compile(buffer.toString());
		} catch (PatternSyntaxException e) {
			return false;
		}
		return true;
	}
	/**
	 * get next char
	 * @param text
	 * @param i
	 * @param length
	 * @return
	 */
	private static char getNext(String text, int i, int length) {
		if (i < length - 1) {
			return text.charAt(i + 1);
		} else {
			return 0;
		}
	}

	private static char getNext(String text, int i, int length, int index) {
		if (i < length - index) {
			return text.charAt(i + index);
		} else {
			return 0;
		}
	}

	private static char getPre(String text, int i, int length) {
		if (i > 0) {
			return text.charAt(i - 1);
		} else {
			return 0;
		}
	}

	private static String getNextString(String text, int i, int length,
			int strLength) {
		StringBuilder builder = new StringBuilder();
		for (int j = 0; j < strLength; j++) {
			if ((i + j + 1) >= length) {
				break;
			}
			builder.append(text.charAt(j + i + 1));
		}
		return builder.toString();
	}

	public static void main(String[] args){
		ScriptCompileEngine engine = new ScriptCompileEngine();
		try {
			FileUtils.writeStringToFile(new File("D:\\eos6\\profile\\test1.html"), engine.compileJs("test",FileUtils.readFileToString(new File("D:\\eos6\\profile\\test.html"))));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}
